package nsfs

import (
	"GLibHac/fssystem"
	"GLibHac/tools"
	"encoding/binary"
	"github.com/pkg/errors"
	"strings"
)

type RomFsEntry struct {
	Parent    int32
	Next      int32
	KeyLength int32
	IsFile    bool
	value     interface{}
}

func (e *RomFsEntry) GetFileRomEntry() *FileRomEntry {
	return e.value.(*FileRomEntry)
}

func (e *RomFsEntry) GetDirectoryRomEntry() *DirectoryRomEntry {
	return e.value.(*DirectoryRomEntry)
}

type RomEntryKey struct {
	Name   string
	Parent int32
}

func NewRomEntryKey(name string, parent int32) *RomEntryKey {
	return &RomEntryKey{name, parent}
}

func (e *RomEntryKey) GetRomHashCode() uint32 {
	hash := 123456789 ^ uint32(e.Parent)
	for _, c := range e.Name {
		hash = uint32(c) ^ ((hash << 27) | (hash >> 5))
	}
	return hash
}

type RomFileInfo struct {
	Offset int64
	Length int64
}

type FindPosition struct {
	NextDirectory int32
	NextFile      int32
}

const (
	DirectoryRomEntrySize = 12
	FileRomEntrySize      = 20
)

type DirectoryRomEntry struct {
	NextSibling int32
	Pos         *FindPosition
}

func NewDirectoryRomEntry(data []byte) *DirectoryRomEntry {
	return &DirectoryRomEntry{
		NextSibling: tools.BytesToIntBigEndian(data),
		Pos: &FindPosition{
			NextDirectory: tools.BytesToIntBigEndian(data[4:]),
			NextFile:      tools.BytesToIntBigEndian(data[8:]),
		},
	}
}

type FileRomEntry struct {
	NextSibling int32
	Info        *RomFileInfo
}

func NewFileRomEntry(data []byte) *FileRomEntry {
	return &FileRomEntry{
		NextSibling: tools.BytesToIntLittleEndian(data),
		Info: &RomFileInfo{
			Offset: int64(binary.LittleEndian.Uint64(data[4:12])),
			Length: int64(binary.LittleEndian.Uint64(data[12:])),
		},
	}
}

type RomFsDictionary struct {
	isFile    bool
	buckets   []int32
	entries   []byte
	length    int32
	capacity  int32
	count     int32
	valueSize int32
}

func (r *RomFsDictionary) GetSizeOfEntry() int32 {
	return r.GetValueSize() + 12
}

func (r *RomFsDictionary) GetValueSize() int32 {
	if r.isFile {
		return FileRomEntrySize
	} else {
		return DirectoryRomEntrySize
	}
}

func (r *RomFsDictionary) CountEntries() int32 {
	count := int32(0)
	nextStructOffset := (4 + r.GetValueSize()) / 4
	data := make([]int32, len(r.entries)/4)
	for i := 0; i < len(data); i++ {
		data[i] = tools.BytesToIntLittleEndian(r.entries[i*4:])
	}
	for _, bucket := range r.buckets {
		next := bucket
		for next != -1 {
			next = data[next/4+nextStructOffset]
			count++
		}
	}
	return count
}

func (r *RomFsDictionary) GetOffsetFromKey(key *RomEntryKey) int32 {
	hashCode := key.GetRomHashCode()
	index := (int32)(hashCode % uint32(len(r.buckets)))
	i := r.buckets[index]

	for i != -1 {
		name, reference := r.GetEntryReference(i)
		if key.Parent == reference.Parent && key.Name == name {
			break
		}
		i = reference.Next
	}

	return i
}

func (r *RomFsDictionary) GetEntryReference(offset int32) (string, *RomFsEntry) {
	valueSize := r.GetValueSize()
	entry := &RomFsEntry{
		IsFile:    r.isFile,
		Parent:    tools.BytesToIntLittleEndian(r.entries[offset:]),
		Next:      tools.BytesToIntLittleEndian(r.entries[offset+valueSize+4:]),
		KeyLength: tools.BytesToIntLittleEndian(r.entries[offset+valueSize+8:]),
	}
	nameOffset := offset + r.GetSizeOfEntry()
	name := string(r.entries[nameOffset : nameOffset+entry.KeyLength])
	offset += 4
	if r.isFile {
		entry.value = NewFileRomEntry(r.entries[offset : offset+FileRomEntrySize])
	} else {
		entry.value = NewDirectoryRomEntry(r.entries[offset : offset+DirectoryRomEntrySize])
	}
	return name, entry
}

func (r *RomFsDictionary) TryGetValue(key *RomEntryKey) (string, *RomFsEntry, error) {
	return r.TryGetValueByOffset(r.GetOffsetFromKey(key))
}

func (r *RomFsDictionary) TryGetValueByOffset(offset int32) (string, *RomFsEntry, error) {
	if offset < 0 || int(offset+r.GetSizeOfEntry()) > len(r.entries) {
		return "", nil, errors.Errorf("invalid offset %d, offset must be between 0 and %d", offset, len(r.entries)-int(r.GetSizeOfEntry()))
	}
	name, entry := r.GetEntryReference(offset)
	return name, entry, nil
}

func NewRomFsDictionary(bucketStorage fssystem.IStorage, entryStorage fssystem.IStorage, isFile bool) (r *RomFsDictionary, err error) {
	r = &RomFsDictionary{
		isFile: isFile,
	}
	r.buckets, err = bucketStorage.ReadAllToInt()
	r.entries, err = entryStorage.ReadAll()
	r.length = int32(len(r.entries))
	r.capacity = r.length
	r.count = r.CountEntries()
	return
}

type HierarchicalRomFileTable struct {
	fileTable      *RomFsDictionary
	directoryTable *RomFsDictionary
}

func (t *HierarchicalRomFileTable) TryOpenFile(filePath string) (*RomFileInfo, error) {
	key := t.FindPathRecursive(filePath)
	_, entry, err := t.fileTable.TryGetValue(key)
	return entry.GetFileRomEntry().Info, err
}

func (t *HierarchicalRomFileTable) FindPathRecursive(path string) *RomEntryKey {
	index := int32(0)
	split := strings.Split(path, "/")
	if len(split) == 0 {
		return NewRomEntryKey("", 0)
	}
	key := NewRomEntryKey(split[index], index)
	for int(index) < len(split)-1 {
		key.Parent = t.directoryTable.GetOffsetFromKey(key)
		index++
		if int(index) < len(split) {
			key.Name = split[index]
		}
	}
	return key
}

func (t *HierarchicalRomFileTable) TryOpenDirectory(path string) (*FindPosition, error) {
	key := t.FindPathRecursive(path)
	_, result, err := t.directoryTable.TryGetValue(key)
	if err != nil {
		return nil, err
	}
	directoryRomEntry := result.GetDirectoryRomEntry()
	return directoryRomEntry.Pos, nil
}

func (t *HierarchicalRomFileTable) FindNextDirectory(position *FindPosition) (string, *DirectoryRomEntry, error) {
	if position.NextDirectory == -1 {
		return "", nil, nil
	}
	name, entry, err := t.directoryTable.TryGetValueByOffset(position.NextDirectory)
	if err != nil {
		return "", nil, err
	}
	return name, entry.GetDirectoryRomEntry(), nil
}

func (t *HierarchicalRomFileTable) FindNextFile(position *FindPosition) (string, *FileRomEntry, error) {
	if position.NextFile == -1 {
		return "", nil, nil
	}
	name, entry, err := t.fileTable.TryGetValueByOffset(position.NextFile)
	if err != nil {
		return "", nil, err
	}
	position.NextFile = entry.GetFileRomEntry().NextSibling
	return name, entry.GetFileRomEntry(), nil
}

func NewHierarchicalRomFileTable(dirHashTable, dirEntryTable, fileHashTable, fileEntryTable fssystem.IStorage) (r *HierarchicalRomFileTable, err error) {
	r = &HierarchicalRomFileTable{}
	r.fileTable, err = NewRomFsDictionary(fileHashTable, fileEntryTable, true)
	if err != nil {
		return nil, err
	}
	r.directoryTable, err = NewRomFsDictionary(dirHashTable, dirEntryTable, false)
	return
}
